##################################################################
# File : /lua/sim/globals.lua
# Description : Global sim functions
# Author : Neruz
##################################################################

local armorTypes = import('/lua/common/ArmorTypes.lua').ArmorTypes

function SimGetEconomyTotals(unit)
    local aiBrain = unit:GetAIBrain()
    local econ = {
        stored = {}
    }
    econ.stored.MASS = aiBrain:GetEconomyStored('MASS')
    return econ
end

# Called by the engine when switching focus armies.
FocusArmyChangedCallback = Callback()
function OnFocusArmyChanged(new,old)
    FocusArmyChangedCallback:Call(new,old)
end

function DealDamage(data,target)
    if not target then
        error('DealDamage called with no target')
    end

    if not data.Amount then
        WARN('DealDamage called with 0 damage amount')
        return
    end

    if not target.BeenDestroyed or target:BeenDestroyed() then
        if not target.BeenDestroyed then
            local stg = 'Globals.lua, target doesn\'t have a :BeenDestroyed() method: ', repr(target)
            WARN(stg)
        end
        return
    end

    if IsBlip(target) then
        target = target:GetSource()
    end

    if target:IsDead() then
        return
    end

    if not data.DamageSelf then
        local inst = data.Instigator
        if inst and (not inst:BeenDestroyed()) and IsProjectile(inst) then
            inst = IsProjectile(inst):GetLauncher()
        end
        # Don't damage the instigator
        if inst and inst == target then
            return
        end
    end

    local newdata = table.copy(data)
    local targetArmor = target.ArmorType or target:GetBlueprint().Defense.ArmorType
    if not data.IgnoreDamageRangePercent then
        newdata.Amount = CalculateDamageWithRange(data.Amount)
    end
    if targetArmor and armorTypes[targetArmor] and armorTypes[targetArmor][data.Type] then
        newdata.Amount = newdata.Amount * armorTypes[targetArmor][data.Type]
    end

    if newdata.Amount != 0 then
        if not newdata.Vector and newdata.Origin then
            newdata.Vector = VDiff(target:GetPosition(),data.Origin)
        end
        target:OnDamage(newdata)
    end
end


function Leech( unit, target, data, amount )
    local healthadj = math.min( target:GetHealth(), amount )
    data.Amount = healthadj
    DealDamage( data, target )
    data.Amount = -healthadj
    DealDamage( data, unit )
end

function AbsorbDamage(entity,shieldHits,damage)
    local d = damage
    for k,hit in shieldHits do
        if hit.Shield:ContainsPoint(entity:GetPosition()) then
            d = d - hit.Absorbed
        end
    end
    return d
end

function GetShieldHits(data)
    local shields = GetShields()
    local hits = {}
    for k,shield in shields do
        if not data.DamageFriendly then
            if IsAlly(data.InstigatorArmy,shield:GetArmy()) then
                continue
            end
        end

        # If the damage originates inside the shield, ignore the shield entirely.
        # It's not the perfect solution, but is much more cost effective performance
        # wise. Note: we also fudge the size of the shield for containment check since
        # we can't get damage positions that are very nearly in or out from projectiles
        # impacting on the surface. If the point is "near" the surface of the shield, we
        # ignore it.
        if shield:IsSphere() then
            local sphere = shield:GetCollisionShape()
            sphere.Radius = sphere.Radius - 0.1
            if PointInSphere(data.Origin,sphere.Center,sphere.Radius) then
                continue
            end
        end

        local hit = shield:IntersectsSphere( Sphere(data.Origin,data.Radius) )

        # If it's ring damage, check against the Max sphere too.
        if data.RadiusMax then
            hit = hit or shield:IntersectsSphere( Sphere(data.Origin,data.RadiusMax) )
        end

        if hit then
            if data.Amount > 0 then
                local val = shield:OnGetDamageAbsorption(data)
                if val > 0 then
                    table.insert(hits,{Shield = shield, Absorbed = val})
                end
            else
                table.insert(hits,{Shield = shield})
            end
        end
    end
    return hits
end

function DamageArea(data, dmgEntities)
    if not data.Radius or data.Radius <= 0 then
        error('DamageArea Radius <=0')
    end

    if not data.Group then
        error('No entity group(s) specified.')
    end

    local absorb = GetShieldHits(data)
    local entities = dmgEntities or GetEntitiesInSphere(data.Group, data.Origin, data.Radius)

    local instArmy = data.InstigatorArmy

    for k,entity in entities do

        if entity:IsDead() then
            continue
        end

        if not data.DamageFriendly and instArmy >= 0 then
            if IsAlly(instArmy,entity:GetArmy()) then
                continue
            end
        end

        if EntityCategoryContains(categories.NOSPLASHDAMAGE,entity) then
            continue
        end

        if data.IgnoreTargets and table.find(data.IgnoreTargets,entity) then
            continue
        end

        local damage = AbsorbDamage(entity,absorb,data.Amount)
        if damage < 0 then
            continue
        end

        local dmgData = table.copy(data)
        dmgData.Amount = damage
        dmgData.Vector = VDiff(entity:GetPosition(),data.Origin)
        DealDamage(dmgData, entity)
    end

    for k,absorb in absorb do
        local dmgData = table.copy(data)
        dmgData.Amount = absorb.Absorbed
        dmgData.Vector = VDiff(absorb.Shield:GetPosition(),data.Origin)
        DealDamage(dmgData, absorb.Shield)
    end
end


#
# Meta Impact
#
function EntityIsShielded(entity, shieldHits)
    for k,hit in shieldHits do
        if hit.Shield:ContainsPoint(entity:GetPosition()) then
            return true
        end
    end
    return false
end

function MetaImpact(data, metaEntities)
    if not data.Radius or data.Radius <= 0 then
        error('MetaImpact Radius <=0')
    end

    if not data.Category then
        error('No category specified.')
    end

    local category = data.Category
    if type(category) == 'string' then
        category = ParseEntityCategoryEx(data.Category)
    end

    local instArmy = data.InstigatorArmy

    local shieldHits = GetShieldHits(data)
    local entities = metaEntities or GetEntitiesInSphere("UNITS", data.Origin, data.Radius)

    for k,entity in entities do

        if entity:IsDead() then
            continue
        end

        if entity.CanTakeImpulse == false then
            continue
        end

        if not entity:IsMobile() then
            continue
        end

        if not EntityCategoryContains(category,entity) then
            continue
        end

        if not data.DamageFriendly and instArmy >= 0 then
            if IsAlly(instArmy,entity:GetArmy()) then
                continue
            end
        end

        if EntityIsShielded(entity,shieldHits) then
            continue
        end

        # Calculate the impact vector to fling the unit around
        local impactVec = VDiff(entity:GetPosition(), data.Origin)
        impactVec.y = 0
        local distToCenter = VLength(impactVec)
        if data.SpecialInnerRadius and distToCenter < data.SpecialInnerRadius then
            data.SpecialInnerRadiusFunc(entity)
            continue
        end
        impactVec.y = data.Radius - distToCenter
        impactVec = VNormal(impactVec)
        impactVec = VMult(impactVec, data.Amount * (1.0 - (distToCenter/data.Radius) * 0.25))

        # Apply the impulse force
        local result = entity:AddImpulseEx(impactVec,true)
        if result and EntityCategoryContains(categories.ALLUNITS, entity) then
            entity.KillData = {
                InstigatorArmy = instArmy,
                Instigator = data.Instigator,
                InstigatorBp = data.InstigatorBp,
                DamageAction = 'MetaImpact',
                KillLocation = table.copy(entity:GetPosition()),
            }
        end
    end
end